<?php
/**
* Nephtali Web Framework
* 
* @author Adam Richardson of Envision Internet Consulting, LLC
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/

/**
* All functions and constants utilized by the Nephtali Framework.
*/
namespace n
{
	/**
	* Value accessor (values are immutable by default.)
	*
	* @staticvar array $values
	* @staticvar array $mutables
	* @param string $name
	* @param mixed $value (OPTIONAL) Only needed when setting a value.
	* @param mixed $is_mutable (OPTIONAL) Defaults to false.
	* @return mixed
	*/
	function val($name, $value = null, $is_mutable = false)
	{
		static $values = array();
		static $mutables = array();

		if ($value === null) {
			if (isset($values[$name])) {
				return $values[$name];
			} else {
				return null;
			}
		} else {
			if (isset($values[$name])) {
				if (!$val_is_mutable = \in_array($name, $mutables)) {
					throw new \Exception('The value "' . $name . '" is immutable and has already been set to '.$value.'.');
				} else {
					$values[$name] = $value;
				}
			} else {
				$values[$name] = $value;
				if ($is_mutable) $mutables[] = $name;
			}
		}
	}
	/**
	* Adds variable value as a watched variable for debugging purposes.
	*
	* @param string name
	* @param mixed $value
	* @return void
	*/
	function watch($name, $value)
	{
		if (!isset($_SESSION))
			\session_start();

		if (!isset($_SESSION['nephtali_debug']))
			$_SESSION['nephtali_debug'] = array();

		// add the watched variable to the most recent nephtali_debug request
		$requests = $_SESSION['nephtali_debug'];
		$values = \array_pop($requests);
		$values['vars'][] = array('name' => $name, 'value' => $value);
		$requests[] = $values;
		$_SESSION['nephtali_debug'] = $requests;
	}
}
/**
* Functions facilitating work with arrays.
*/
namespace n\arr
{
	/**
	 * Return HTML-formatted output of array contents.
	 *
	 * Returns an HTML-formatted presentation of array contents using an unordered list.  Array values are automatically escaped using n\str\encode() function.
	 *
	 * @param array $arr
	 * @return string
	 */
	function dump(array $arr)
	{
		$output = '<ul>';

		if ($arr) {
			foreach ($arr as $key => $val) {
				$output .= "<li><strong>Key:</strong> $key";

				if ((array)$val === $val) {
					$output .= '<br />'.dump($val).'</li>';
				} else {
					$output .= ', <strong>Value:</strong> '.(($val === null) ? 'NULL' : \n\str\encode($val)).'</li>';
				}
			}
		} else {
			$output .= 'EMPTY';
		}

		return $output.'</ul>';
	}
	/**
	* Formats an array recursively searching for keys and then applying the formatting function.
	*
	* @param array $array
	* @param array $key_functions Associative array of 'keys' and the corresponding functions to act on the keys (functions accept a single and return a formatted value.)
	* @return array
	*/
	function format(array $array, array $key_functions)
	{
		$new_array = array();

		foreach ($array as $key => $val) {
			if ((array)$val === $val) {
				$new_array[$key] = format($array = $val, $key_functions);
			} else {
				if (isset($key_functions[$key])) {
					$func = $key_functions[$key];
					$new_array[$key] = $func($val);
				} else {
					$new_array[$key] = $val;
				}
			}
		}

		return $new_array;
	}
	/**
	 * Determine the type of array (indexed, associative, mixed, or empty.)
	 * 
	 * @param array $arr
	 * @return string Possible values are 'indexed', 'associative', 'mixed', and 'empty'.
	 */
	function get_type(array $arr)
	{
		$type = 'empty';

		foreach ($arr as $key => $val){
			if ((int)$key === $key) {
				switch ($type) {
					case 'empty':
						$type = 'indexed';
						break;
					case 'indexed':
						break;
					case 'associative':
						// we know it's mixed, so let's scram
						return $type = 'mixed';
				}
			} else {
				$type = 'associative';
			}
		}

		return $type;
	}
	/**
	 * Return array containing values forming the union of array values for two or more (up to 5) arrays.
	 * 
	 * @param array $array1
	 * @param array $array2
	 * @param array $array3 (OPTIONAL)
	 * @param array $array4 (OPTIONAL)
	 * @param array $array5 (OPTIONAL)
	 * @return array
	 */
	function union(array $array1, array $array2, array $array3 = array(), array $array4 = array(), array $array5 = array())
	{
		return \array_unique(\array_merge($array1, $array2, $array3, $array4, $array5));
	}
}
/**
* All Nephtali constants.
*/
namespace n\constant
{
	/**
	* Cache the final output provided by the page.
	*/
	const CACHE_FINAL = 47;
	/**
	* Cache the output after all templates have been applied.
	*/
	const CACHE_AFTER_TEMPLATES = 48;
	/**
	* Allow integers.
	*
	* Examples include:
	* - 1
	* - 65000
	*/
	const FILTER_INT = 1;
	/**
	* Allow positive or negative numbers with decimal points.
	*
	* Examples include:
	* - 2.5
	* - 890.009
	* - -23.9
	*/
	const FILTER_DOUBLE = 2;
	/**
	* Allow alphanumeric text.
	*
	* Examples include:
	* - abc678
	* - 55uugr
	*/
	const FILTER_ALPHANUM = 3;
	/**
	* Allow characters typically used in variable names.
	*
	* Examples include:
	* - frank_the_terminator
	* - symbol78
	*/
	const FILTER_VAR_NAME = 19;
	/**
	* Allow valid email addresses.
	*/
	const FILTER_EMAIL = 4;
	/**
	* Allow valid URL's.
	*
	* Examples include:
	* - http://test.com
	* - http://www.test.com
	* - http://subdomain.test.com/index.php?var=value&var2=yes
	*/
	const FILTER_URL = 5;
	/**
	* Allow valid US dates of the format m/d/yyyy.
	*
	* Examples include:
	* - 9/7/2008
	* - 09/09/2009
	*/
	const FILTER_US_DATE = 22;
	/**
	* Allow valid US zip codes.
	*
	* Examples include:
	* - 55555
	* - 55555-5555
	*/
	const FILTER_US_ZIP = 10;
	/**
	* Allow valid US phone numbers.
	*
	* Examples include:
	* - 5555555555
	* - 1-555-555-5555 (1 is optional)
	* - 1(555)555-5555 (1 is optional)
	* - 1 (555) 555-5555 (1 is optional)
	* - 1 555 555 5555 (1 is optional)
	*/
	const FILTER_US_PHONE = 11;
	/**
	* Allow a single line of text except for XML markup and danerous hackemes.
	*/
	const FILTER_TEXT = 7;
	/**
	* Allow multiple lines of text except for XML markup and dangerous hackemes.
	*/
	const FILTER_TEXT_MULTILINE = 9;
	/**
	* Allow all types of text except for dangerous hackemes.
	*/
	const FILTER_XML = 6;
	/**
	* Allow everything under the sun.
	*/
	const FILTER_ALL = 8;
	/**
	* Allow data that validates with custom regex (you must supply the regex in the opts array for the port.)
	*/
	const FILTER_CUSTOM = 21;
	/**
	* Regex used for validating positive integers.
	*/
	const REGEX_INT = '/^\d+$/';
	/**
	* Regex used for validating double numbers.
	*/
	const REGEX_DOUBLE = '/^-?\d+(\.\d+)?$/';
	/**
	* Regex used for validating alphanumeric data (a - z, 0 - 9, case insensitive.)
	*/
	const REGEX_ALPHANUM = '/^[a-zA-Z0-9]+$/';
	/**
	* Regex used for validating variable names (a - z, 0 - 9, '_', '-', case insensitive.)
	*/
	const REGEX_VAR_NAME = '/^[-a-zA-Z0-9_]+$/';
	/**
	* Regex used for validating US phone numbers.
	*/
	const REGEX_US_PHONE = '/^((1)?[2-9]\d{9})|((((1)?[ ]?\([2-9]\d{2}\)[ ]?)|((1-)?[2-9]\d{2}-))\d{3}-\d{4})|((1[ ])?\d{3}[ ]\d{3}[ ]\d{4})$/';
	/**
	* Regex used for validating US Zip codes.
	*/
	const REGEX_US_ZIP = '/^\d{5}((-)?\d{4})?$/';
	/**
	* Regex used for validating basic text, free of HTML special characters.
	*/
	const REGEX_TEXT = '/^[ \/a-zA-Z0-9.!@#$%&*:;?()_+=\'",\[\]\-]+$/';
	/**
	* Regex used for validating basic multiline text, free of HTML special characters.
	*/
	const REGEX_TEXT_MULTILINE = '/^[ \/a-zA-Z0-9.!@#$%&*:;?()_+=\'"\n\r,\[\]-]+$/';
	/**
	* Regex used for validating text that contains XML.
	*/
	const REGEX_XML = '/^(.|\n)+$/';
	/**
	* Regex used for file names, which allows letters, numbers, whitespace, '.', and the underscore.
	*/
	const REGEX_FILE_NAME = '/^[-a-zA-Z0-9_\s\.]+$/';
	/**
	* Regex used for URL's.
	*/
	const REGEX_URL = '/^(http|https):\/\/(([-a-zA-Z0-9]+)\.)*([-a-zA-Z0-9]+)\.([a-z]{2,6})(\/([-a-zA-Z0-9#_.=?+&;:%!])*)*$/';
	/**
	* Regex used for email addresses.
	*/
	const REGEX_EMAIL = '/^[a-z0-9!#$%&*+=?^_`{|}~-]+(?:\.[a-z0-9!#$%&*+=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+([a-z]{2,6})$/';
	/**
	* Whitelist regex used to allow links through automatic encoding.
	*/
	const WHITELIST_A_TAG = '/<a href="(((http|https):\/\/)|(mailto:[-a-zA-Z0-9#_.+]*@))(([-a-zA-Z0-9]+)\.)*([-a-zA-Z0-9]+)\.([a-z]{2,6})(\/([-a-zA-Z0-9#_.=?+&;:%!])*)*">[ a-zA-Z0-9,)$&;(\.\?!@#%-]+<\/a>/';
	/**
	* Whitelist regex that allows everything to be passed through the databinding process without automatic encoding.
	*/
	const WHITELIST_ALLOW_ALL = 1024;
}
/**
* Cache functions.
*/
namespace n\cache
{
	/**
	 * Register caching functions for one of the stages in the page rendering process.
	 * 
	 * @param string $stage
	 * @param mixed $get_func
	 * @param mixed $set_func
	 * @return void
	 */
	function register($stage, $get_func, $set_func)
	{
		\n\z\value($domain = 'cache', $command = 'set', $inputs = array('stage' => $stage, 'get_func' => $get_func, 'set_func' => $set_func), $index = 'stage');
	}
}
/**
* Configuration file value functions.
*/
namespace n\config
{
	/**
	* Provides access to stored config values.
	*
	* @param mixed $names If an array of names, returns an array of config values.  If one name, returns that one value.
	* @return mixed
	*/
	function get($names)
	{
		static $values = 'empty';

		if ((array)$names === $names) {
			if ($values == 'empty') {
				$values = $names;
				return;
			} else {
				// if array, return array
				return \array_intersect_key($values, \array_fill_keys($names, 0));
			}
		} else {
			// if singular value, return one value
			return $values[$names];
		}
	}
	/**
	* Primes config() function so subsequent calls to get() retrieve the value associated with the argument key.
	*
	* @param array $mode
	* @param array $dev
	* @param array $test
	* @param array $prod
	* @param bool  $allow_page_overrides (OPTIONAL) Set to true if you want to allow pages to override the mode in the markup. When false, overrides have no effect.
	* @return void
	*/
	function save($mode, array $dev, array $test, array $prod, $allow_page_overrides = false)
	{
		// mode can be set on page-by-page basis, too
		global $nephtali_config_mode;

		$mode = (isset($nephtali_config_mode) && ($allow_page_overrides)) ? $nephtali_config_mode : $mode;

		$defaults = array(
			'mode'						=> 'dev',
			'charset'					=> 'UTF-8',
			'error_page'				=> '/error.html',
			'encryption_key'			=> null,
			'encryption_salt'			=> null,
			'upload_directory'			=> '',
			'upload_ftp_url'			=> null,
			'sql_action_dsn'			=> null,
			'sql_action_username'		=> null,
			'sql_action_password'		=> null,
			'sql_datasource_dsn'		=> null,
			'sql_datasource_username'	=> null,
			'sql_datasource_password'	=> null
		);

		switch ($mode) {
			case 'dev':
				$s = $dev;
				break;
			case 'test':
				$s = $test;
				break;
			case 'prod':
				$s = $prod;
				break;
			default:
				throw new \Exception('The mode must be one of the following strings: dev, test, prod.');
		}

		$s['mode'] = $mode;
		get(\array_merge($defaults, $s));
	}
}
/**
* Function wrappers for CURL operations such as getting contents of files, uploading via FTP, etc.
*/
namespace n\curl
{
	/**
	* FTP's a file to a server via curl.
	*
	* @param string $local_file_path
	* @param string $file_name
	* @param string $ftp_domain
	* @param string $ftp_username
	* @param string $ftp_password
	* @return void
	*/
	function ftp_upload($local_file_path, $local_file_size, $file_name, $ftp_domain, $ftp_username, $ftp_password)
	{
		$fp = \fopen($local_file_path, 'r');
		$ch = \curl_init();
		\curl_setopt($ch, \CURLOPT_URL, 'ftp://'.\urlencode($ftp_username).':'.\urlencode($ftp_password).'@'.$ftp_domain.$file_name);
		\curl_setopt($ch, \CURLOPT_UPLOAD, 1);
		\curl_setopt($ch, \CURLOPT_INFILE, $fp);
		\curl_setopt($ch, \CURLOPT_INFILESIZE, $local_file_size);
		// needed so output isn't echo'd back
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
		\curl_exec($ch);
		$error_no = \curl_errno($ch);
		\curl_close ($ch);

		if ($error_no != 0) {
			if ((int)$error_no === $error_no) {
				throw new \Exception('There was an error uploading the file \''.$file_name.'\'. CURL error number:'.$error_no);
			} else {
				throw new \Exception('There was an error uploading the file, and the CURL error number was invalid, too.');
			}
		}
	}
	/**
	* Save a file upload using CURL to ftp to the site and upload the file.
	*
	* This is useful on shared hosts where one does not have permissions to save files with PHP. In this case, you can set up an FTP account to an uploads directory, and then use this function to upload the file to the directory.
	*
	* @param string $post_key
	* @param string $file_name
	* @param string $ftp_domain
	* @param string $ftp_username
	* @param string $ftp_password
	* @return void
	*/
	function save_upload($post_key, $file_name, $ftp_domain, $ftp_username, $ftp_password)
	{
		ftp_upload($local_file_path = $_FILES[$post_key]['tmp_name'], $local_file_size = $_FILES[$post_key]['size'], $file_name, $ftp_domain, $ftp_username, $ftp_password);
	}
	/**
	* Retrieves the contents of a url via CURL.
	*
	* Unlike file_get_contents(), this function is useful because you can set a timeout on the request, which is useful in situations where you don't want an entire page to error out just because of one section of the page.
	*
	* @param string $url
	* @param array $post_data (OPTIONAL)
	* @param int $connect_timeout (OPTIONAL) Defaults to 1 second.
	* @param int $transaction_timeout (OPTIONAL) Defaults to 2 seconds.
	* @param array $opts (OPTIONAL) Any other curl options (e.g., CURLOPT_REFERER, CURLOPT_USERAGENT, etc.)
	* @return string
	*/
	function get_contents($url, array $post_data = array(), $connect_timeout = 1, $transaction_timeout = 2, $opts = array())
	{
		$ch = \curl_init();
		\curl_setopt($ch, \CURLOPT_URL, $url);
		\curl_setopt($ch, \CURLOPT_CONNECTTIMEOUT, $connect_timeout);
		\curl_setopt($ch, \CURLOPT_TIMEOUT, $transaction_timeout);
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);

		if ($post_data) {
			\curl_setopt($ch, \CURLOPT_POST, 1);
			\curl_setopt($ch, \CURLOPT_POSTFIELDS, $post_array);
		}

		// add other options
		if ($opts)
			\curl_setopt_array($ch, $opts);

		$str = \curl_exec($ch);
		$error_no = \curl_errno($ch);
		\curl_close ($ch);

		if ($error_no != 0)
			throw new \Exception('There was an error retreiving the string contents of the url \''.$url.'\'. CURL error number:'.$error_no);

		return $str;
	}
	/**
	* Retrieves the contents of a multiple urls in parallel via CURL.
	*
	* In situations where some tasks take a significant amount of time relative to others, processing the tasks in parallel can buy you an overall improvement in performance, as the total time now equals the time for the longest task as opposed to the total time for each of the indivual tasks.
	*
	* @param array $requests An array of requests, with each request containing an associative array with the values url (required string), post_data (array), connect_timeout (int), transaction_timeout (int).
	* @return array
	*/
	function get_contents_parallel(array $requests)
	{
		$chs = array();
		$responses = array();
		$mh = \curl_multi_init();
		$running = null;

		foreach ($requests as $request) {
			$chs[$request['url']] = \curl_init();
			\curl_setopt($chs[$request['url']], \CURLOPT_URL, $request['url']);
			\curl_setopt($chs[$request['url']], \CURLOPT_CONNECTTIMEOUT, $request['connect_timeout']);
			\curl_setopt($chs[$request['url']], \CURLOPT_TIMEOUT, $request['transaction_timeout']);
			\curl_setopt($chs[$request['url']], \CURLOPT_RETURNTRANSFER, 1);

			if ($request['post_data']) {
				\curl_setopt($chs[$request['url']], \CURLOPT_POST, 1);
				\curl_setopt($chs[$request['url']], \CURLOPT_POSTFIELDS, $request['post_array']);
			}

			if ($request['opts'])
				\curl_setopt_array($ch[$request['url']], $request['opts']);

			\curl_multi_add_handle($mh, $chs[$request['url']]);
		}

		do {
			\curl_multi_exec($mh, $running);
		} while($running > 0);

		foreach ($chs as $url => $ch) {
			$responses[$url] = \curl_multi_getcontent($ch);
			\curl_multi_remove_handle($mh, $ch);
		}

		\curl_multi_close($mh);
		return $responses;
	}
}
/**
 * Function(s) for integrating curl requests in a manner that allows for automatic parallel processing.
 */
namespace n\curl\request
{
	/**
	* Registers a web request for parallel processing using CURL.
	*
	* @param string $url
	* @param array $post_data (OPTIONAL)
	* @param int $connect_timeout (OPTIONAL) Defaults to 1 second.
	* @param int $transaction_timeout (OPTIONAL) Defaults to 2 seconds.
	* @param array $opts (OPTIONAL) Any other curl options (e.g., CURLOPT_REFERER, CURLOPT_USERAGENT, etc.)
	* @return void
	*/
	function register($url, array $post_data = array(), $connect_timeout = 1, $transaction_timeout = 2, $opts = array())
	{
		\n\z\value($domain = 'request', $command = 'set', $inputs = array('url' => $url, 'post_data' => $post_data, 'connect_timeout' => $connect_timeout, 'transaction_timeout' => $transaction_timeout, 'opts' => $opts), $index = 'url');
	}
}
/**
 * Function(s) for integrating curl responses in a manner that allows for automatic parallel processing.
 */
namespace n\curl\response
{
	/**
	* Retrieves the response from the corresponding web request.
	*
	* @param string $url
	* @return string
	*/
	function get($url)
	{
		$url_is_set = \n\z\value($domain = 'response', $command = 'isset', $inputs = array($url));

		if ($url_is_set[$url]) {
			$responses = \n\z\value($domain = 'response', $command = 'get', $inputs = array($url));
			return $responses[$url]['output'];
		}

		$req_settings = \current(\n\z\value($domain = 'request', $command = 'get', $inputs = array($url)));
		return \n\curl\get_contents($url = $req_settings['url'], $post_data = $req_settings['post_data'], $connect_timeout = $req_settings['connect_timeout'], $transaction_timeout = $req_settings['transaction_timeout'], $opts = $req_settings['opts']);
	}
}
/**
* Page markup functions.
*/
namespace n\markup
{
	/**
	* Returns the array of sections formed by a pipe, view, or data region in the markup.
	*
	* @param string $markup
	* @param string $type
	* @param string $name (OPTIONAL)
	* @return array
	*/
	function get_sections($markup, $type, $name = null)
	{
		static $cached_mode = null;
		// avoid having to call n\config\get multiple times
		if ($cached_mode === null)
			$cached_mode = \n\config\get($names = 'mode');

		if ($type != 'data' && \count($sections = \explode('<!--'.$type.':'.$name.'-->', $markup)) == 3) {
			// if in dev mode, check for error view, the one required view.
			if (($type == 'pipe') && ($cached_mode == 'dev') && (\count(\explode('<!--view:error-->', $sections[1])) != 3))
				throw new \Exception("The pipe $name does not have the required error view");

			return $sections;
		}

		if ($type == 'data') {
			return (\count($sections = \explode('<!--data-->', $markup)) == 3)? $sections: array($markup);
		}
		// if we made it here, things aren't as expected
		throw new \Exception('The following '.$type.' could not be properly found in the markup: '.$name);
	}
}
/**
* Pipe functions.
*
* In Nephtali, Pipes are responsible for rendering dynamic regions in a page,
* and controlling the general flow of an application (login mechanisms can be handled by pipes
* without output.)
*/
namespace n\pipe
{
	/**
	* Retrieve the settings for registered pipes.  With no argument, all pipes are retrieved.
	*
	* @param mixed $names (OPTIONAL) If an array of names is provided, an array of pipes is returned.  If one name is provided (string), then that one pipe is returned.  Default is an empty array, which leads to returning all registered pipes.
	* @return mixed
	*/
	function get($names = array())
	{
		if ((array)$names === $names)
			return \n\z\value($domain ='pipe', $command = 'get', $inputs = $names);

		return current(\n\z\value($domain = 'pipe', $command = 'get', $inputs = array($names)));
	}
	/**
	 * Register a pipe for the page.
	 *
	 * The $opts array can contain the options displayed in the example below:
	 * <code>
	 * n\pipe\register(
	 * $name = 'workers',
	 * $function = function($markup, $args, $signatures) {
	 *	// code run within function
	 * },
	 * $opts = array(
	 *	// normally nephtali renders pipes in the order registered, but htmlfrag requests sometimes involve pipes in which other pipes are nested
	 *	'required_pipes' => array('drop_down_menu'),
	 *	// port signatures automatically passed into the function as the $signatures argument after being automatically checked for signature and validated
	 *	'signatures' => array('select' => array('city', 'occupation')),	
	 *	// by default, pipes are required to have corresponding sections in markup, but you can set 'has_output' to false if the pipe truly has no output needs (e.g., authentication)
	 *	'has_output' => false,
	 *	// by default, registered pipes that have output must have corresponding markup present in the page, but you can set 'is_optional' to true if you want a pipe to be rendered only if the page has corresponding markup (i.e., the opening and closing pipe comments are present.)
	 *	'is_optional' => true
	 * ),
	 * // nephtali processes the $args functions when the value in the $args array is first accessed, making it easy to keep logic in the pipe function and keep IO out;
	 * $args = array(
	 *		'sql_query_results' => function($signatures) {
	 *			return n\sql\source\query('SELECT * FROM listings WHERE occupation = :occupation AND city = :city', $inputs = $signatures['select']['values']);
	 *		}
	 * );
	 * </code>
	 *
	 * @param string $name Name of the pipe.
	 * @param mixed $function Rendering function (either string name or a reference to the function, or an anonymous function.)
	 * @param array $opts (OPTIONAL) Associative array containing optional settings (e.g., required_pipes, signatures, has_output, is_optional.)
	 * @param array $args (OPTIONAL) Associative array containing optional functions that should be called at runtime, the results of which are returned in the $args array as an argument to the pipe function.
	 * @return void
	 */
	function register($name, $function, array $opts = array(), array $args = array())
	{
		// merge with defaults
		$opts = \array_merge(array('required_pipes' => array(), 'signatures' => array(), 'has_output' => true, 'is_optional' => false), $opts);
		return \n\z\value($domain = 'pipe', $command = 'set', $inputs = array('name' => $name, 'function' => $function, 'opts' => $opts, 'args' => $args));
	}
	/**
	* Register a pipe that conditionally runs any one of several functions depending upon the ports present on a particular request.
	*
	* @param string $name Name of the pipe.
	* @param array $actions Associative array with keys formed by calls to n\port\signature() (either 0 or a unique index) and values representing the function to run if the signature is valid.  Only the first valid signature is run, so order is important.
	* @param array $opts (OPTIONAL) Associative array containing optional settings (e.g., required_pipes, has_output.)
	* @return void
	*/
	function register_action($name, array $actions, array $opts = array())
	{
		foreach ($actions as $signature => $function) {
			if ($signature) {
				if (isset($opts['required_ports'])) {
					$opts['required_ports'] = \n\arr\union($array1 = $opts['required'], $array2 = \n\z\port\valid_signatures($id = $signature));
				} else {
					$opts['required_ports'] = \n\z\port\valid_signatures($id = $signature);
				}
				
				return register($name, $function, $opts);
			}
		}

		register($name, function($markup = ''){return '';}, $opts);
	}
}
/**
* Port functions
*
* In Nephtali, ports are responsible for validing Get, Post, Cookie, and File inputs.
*/
namespace n\port
{
	/**
	* Retrieve an array of the port settings for the ports named in the array.  If no second argument is provided, by default the value of the port is returned.
	*
	* @param mixed $names (OPTIONAL) If provided an array of names, an array of port values is returned.  If provided one name, one port value is returned.
	* @param string $key (OPTIONAL) Key of the port array element to retrieve (defaults to 'value'.)
	* @return array
	*/
	function get($names = array(), $key = 'value')
	{
		$ports = \n\z\value($domain = 'port', $command = 'get', $inputs = ((array)$names === $names)? $names: array($names));
		$new_ports = array();

		foreach ($ports as $name => $port) {
			if (($key == 'value') && (!$port['valid']))
				throw new \Exception('You cannot retrieve the value of an invalid port ("'.$name.'") with n\port\get().  Check to make sure you are properly checking port validity of your port with the function n\port\validate().');

			$new_ports[$name] = $port[$key];
		}
		// if sent array, return array.
		if ((array)$names === $names) {
			return $new_ports;
		} else {
			// And if sent single value, return single value
			return \current($new_ports);
		}
	}
	/**
	* Register a port for use within pipe functions.
	*
	* The $opts argument provides you with several validation and formatting options, all of which are displayed in the example $opts array below:
	* <code>
	* $opts = array(
	*	// minimum length of submitted string
	*	// a value of zero makes field optional
	*	'min_length' => 0,
	*	// maximum length of submitted string
	*	'max_length' => 5,
	*	// min value, only applies to FILTER_INT and FILTER_DOUBLE
	*	'min_value' => 1,
	*	// min value, only applies to FILTER_INT and FILTER_DOUBLE
	*	'max_value' => 10000000,
	*	// automatically decrypts value before performing validation
	*	'encrypted' => true,
	*	// allows value to be valid even if no GET or POST variable submitted
	*	'allow_null' => true,
	*	// allow array values (e.g, name="buttons[]")
	*	'allow_array' => true,
	*	// provide means for 1 or more custom validators
	*	'validators' => array(
	*		'This will be the error message displayed if the validator returns false.'  => function($val)
	*		{
	*			//add your validating code that returns true (valid) or false (invalid)
	*		}
	*	),
	*	// provide opportunity to format value prior to validation
	*	'formatter' => function($val)
	*	{
	*		//add your formatting code that returns the formatted value
	*		return $val;
	*	}
	* );
	* </code>
	*
	* @param string $name
	* @param array $opts (OPTIONAL) Settings include min_length, max_length, min_value, max_value, filter, regex, allow_null, allow_array, encrypted, error_messages, formatted_name, validators, and formatter.
	* @param mixed $value (OPTIONAL) When null (default value), $_POST value used if exists, otherwise $_GET value used if exists, and if not null is used as the value.
	* @return void
	*/
	function register($name, array $opts = array(), $value = null)
	{
		static $cached_mode = null;
		// avoid having to call n\config\get multiple times
		if ($cached_mode === null)
			$cached_mode = \n\config\get($names = 'mode');

		if ($value === null) {
			$value = (isset($_POST[$name])) ? $_POST[$name] : ((isset($_GET[$name])) ? $_GET[$name] : null);
		}
		
		// in dev mode, make sure there isn't a typo for one of the arrays
		if ($cached_mode == 'dev') {
			$valid_opt_keys = array('min_length', 'max_length', 'min_value', 'max_value', 'filter', 'regex', 'allow_null', 'allow_array', 'encrypted', 'error_messages', 'formatted_name', 'validators', 'formatter','whitelist');
			$opt_keys = array_keys($opts);

			foreach ($opt_keys as $opt_key) {
				if (!\in_array($opt_key, $valid_opt_keys)) {
					$current_suggestion = null;
					$current_similarity_score = 0;

					foreach ($valid_opt_keys as $valid_opt_key){
						if (($similarity_score = \similar_text($first = $opt_key, $second = $valid_opt_key)) > $current_similarity_score) {
							$current_similarity_score = $similarity_score;
							$current_suggestion = $valid_opt_key;
						}
					}

					if ($current_suggestion !== null) {
						$message = 'Port "' . $name . '" has an invalid $opt key: '. $opt_key. '. Did you mean to enter the key "' . $current_suggestion . '" instead?';
						throw new \Exception($message);
					} else {
						$message = 'Port "' . $name . '" has an invalid $opt key: '. $opt_key. '.';
						throw new \Exception($message);
					}
				}
			}
		}
		
		return \n\z\port\save(array('name' => $name, 'value' => $value, 'opts' => $opts, 'type' => 'char_data'));
	}
	/**
	 * Register multiple ports in one call.
	 * 
	 * See n\port\register for a list of valid port options.
	 * <code>
	 * n\port\register(
	 *	$ports = array(
	 *		'id' => array(
	 *			'min_value' => 0,
	 *			'max_value' => 1000000000
	 *		),
	 *		'email' => array(
	 *			'max_length' => 300,
	 *			'filter' => n\constant\FILTER_EMAIL
	 *		),
	 *		'token' => array(
	 *			'max_length' => 200,
	 *			'filter' => n\constant\FILTER_TEXT
	 *		)
	 *	),
	 *	// manually set value for 'token'
	 *	$values = array(
	 *		'token' => $_COOKIE['special_token']
	 *	),
	 *	// only register 'id' and 'token' ports contained within $ports array
	 *	$port_names = array('id','token')
	 * );
	 * </code>
	 * 
	 * @param array $ports Associative array in which keys represent port names and their values represent the port options array.
	 * @param array $values (OPTIONAL) Associative array that allows you to manually add a value for a port by adding the name of the port as the key and the corresponding value. Ports without values explicitly set default to POST, then GET, and then null if neither value was present (default is empty array.)
	 * @param array $port_names (OPTIONAL) Array containing the names of the ports in the $ports argument to be registered [i.e., array_intersect_key($ports, array_fill_keys($port_names, 0)) is performed behind the scenes]
	 * @return array Returns array containing keys representing names of registered ports.
	 */
	function register_bulk($ports, $values = array(), $port_names = array())
	{
		$ports = ($port_names) ? (array_intersect_key($ports, array_fill_keys($port_names, 0))) : ($ports);
		
		foreach ($ports as $name => $opts) {
			register($name, $opts, ((isset($values[$name]) ? $values[$name] : null)));
		}
		
		return array_keys($inputarray = $ports);
	}
	/**
	* Register a file port for use wihtin pipe functions.
	*
	* @param string $name
	* @param array $opts (OPTIONAL)
	* @param mixed $value (OPTIONAL) When null (default value), $_FILES value matching name is used, otherwise null is used as value.
	* @return void
	*/
	function register_file($name, array $opts = array(), $value = null)
	{
		$value = (isset($_FILES[$name])) ? $_FILES[$name] : null;

		if (isset($opts['max_bytes'])) {
			$opts['max_bytes'] = \n\z\bytes\compute($val = $opts['max_bytes']);
			if (($opts['max_bytes'] > \n\z\bytes\compute(\ini_get('upload_max_filesize'))) || ($opts['max_bytes'] > \n\z\bytes\compute(\ini_get('post_max_size')))) {
				throw new \Exception($message = "The port '$name' has max_bytes set to a value larger than PHP allows for file uploads (check the php.ini settings 'upload_max_filesize' and/or 'post_max_size'.");
			}
		}

		if (isset($opts['min_bytes'])) {
			$opts['min_bytes'] = \n\z\bytes\compute($val = $opts['min_bytes']);
		}

		return \n\z\port\save(array('name' => $name, 'value' => $value, 'opts' => $opts, 'type' => 'file'));
	}
	/**
	* Determine if the array of ports are set on the given page request.
	*
	* This is useful if you only want to insert an element if there's no id, or update an element if there is, etc.
	*
	* @param array $ports Array of port names to check.
	* @return int Reterns 0 if the signature is invalid, or a unique index of the number of times n\port\signature has been called (useful for actions pipe.)
	*/
	function signature(array $ports)
	{
		static $counter = 1;
		// stores for later retrieval so you can set required ports for pipe, even if action pipe is registered
		static $valid_signatures = array();

		foreach (\n\z\value($domain = 'port', $command = 'get', $inputs = $ports) as $port) {
			if (!$port['signature'])
				return 0;
		}

		return \n\z\port\valid_signatures($id = $counter++, $val = $ports);
	}
	/**
	* Validate the group of ports named in the array.
	*
	* @param array $ports (OPTIONAL) When not set, all registered ports are validated.
	* @return array $rows Array of associative arrays contained containing port names and error messages (array is empty if all ports were valid.)
	*/
	function validate(array $ports = array())
	{
		$rows = array();
		$retrieved_ports = \n\z\value($domain = 'port', $command = 'get', $inputs = $ports);

		foreach ($retrieved_ports as $name => $port) {
			if ($port['error_message'] !== null)
				$rows[$port['name']] = array('message' => $port['error_message'], 'error_message' => $port['error_message'], 'error' => $port['error_message']);
		}

		return $rows;
	}
}
/**
 * Functions that handle the processing of rows throughout the databinding operations.
 */
namespace n\row
{
	/**
	* Databinds associative array row to the markup according to the mappings.
	*
	* @param string $markup
	* @param array $row
	* @param array $opts (OPTIONAL)
	* @return string
	*/
	function databind($markup, $row, $opts = array())
	{
		$searches = array();
		$replaces = array();

		foreach ($row as $key => $value) {
			// only run auto encoding if found
			if (\strpos($haystack = $markup, $needle = '{'.$key.'}') !== false) {
				// regex replace with expanded form
				$re_patterns = array(
					'/(<[^>]*(?:action|cite|codesource|data|href|longdesc|src|value)=\'[-a-zA-Z0-9#_.=?+&;:%\/]+)({'.$key.'})([-a-zA-Z0-9#_.=?+&;:%\/]*\'[^>]*>)/',
					'/(<[^>]*(?:action|cite|codesource|data|href|longdesc|src|value)="[-a-zA-Z0-9#_.=?+&;:%\/]+)({'.$key.'})([-a-zA-Z0-9#_.=?+&;:%\/]*"[^>]*>)/',
					'/(<[^>]+)({'.$key.'})([^>]*>)/'
				);
				$re_replaces = array(
					'\1{'.$key.'|url}\3',
					'\1{'.$key.'|url}\3',
					'\1{'.$key.'|attr}\3'
				);
				// preg_replace can return null, so must make sure markup isn't lost
				$markup = (($temp_markup = \preg_replace($pattern = $re_patterns, $replacement = $re_replaces, $subject = $markup)) !== null) ? $temp_markup: $markup;
				// everything else is just html
				$markup = \str_replace($search = '{'.$key.'}', $replace = '{'.$key.'|html}', $subject = $markup);
			}

			if (isset($opts['whitelists'][$key]) && $opts['whitelists'][$key]) {
				if ($opts['whitelists'][$key][0] == \n\constant\WHITELIST_ALLOW_ALL) {
					$url_encoded_string = $value;
				} else {
					$url_encoded_string = \n\str\encode($value, $opts['whitelists'][$key]);
				}
				$attr_encoded_string = \n\str\encode($value);
			} else {
				$attr_encoded_string = $url_encoded_string = \n\str\encode($value);
			}
			// html encoding with whitelisting utilized if present
			$searches[] = '{'.$key.'|html}';
			$replaces[] = $url_encoded_string;
			// html encoding without whitelisting
			$searches[] = '{'.$key.'|attr}';
			$replaces[] = $attr_encoded_string;
			// url encoding
			$searches[] = '{'.$key.'|url}';
			$replaces[] = \rawurlencode($value);
		}

		return \str_replace($search = $searches, $replace = $replaces, $subject = $markup);
	}
	/**
	* Databinds associative array row to the markup matching array keys to form name attributes.
	*
	* @param string $markup
	* @param array $row
	* @param array $opts (OPTIONAL)
	* @return string
	*/
	function databind_form($markup, $row, $opts = array())
	{
		foreach ($row as $key => $value) {
			$matches = array();

			if (\preg_match_all($regex = "/<input[^>]*((name=\"$key\")|(name='$key'))[^>]*>/s", $markup, $matches)) {
				// matches[0] is array
				$types = array();
				if (\preg_match('/type=(\'|")([a-zA-Z0-9_-]+)(\'|")/', $matches[0][0], $types)) {
					$type = $types[2];

					switch ($type) {
						case 'radio':
							$search = 'value="'.$value.'"';
							foreach ($matches[0] as $elem) {
								if (\strpos($elem, $search) !== false) {
									// make sure it didn't already have a select
									$new_element = \preg_replace('/checked=(\'|")checked(\'|")/', '', $elem);
									$new_element = \str_replace(" type=", " checked=\"checked\" type=", $new_element);
									$markup = \str_replace($elem, $new_element, $markup);
									break;
								}
							}
							break;
						case 'checkbox':
							// only set if value is true
							if ($value) {
								// make sure it didn't already have a select
								$new_element = \preg_replace('/checked=(\'|")checked(\'|")/', '', $matches[0][0]);
								$new_element = \str_replace(" type=", " checked=\"checked\" type=", $new_element);
								$markup = \str_replace($matches[0][0], $new_element, $markup);
							}
							break;
						case 'file':
							// do nothing for file inputs
							break;
						// handle color, date, datetime, datetime-local, email, hidden, image, month, number, passwors, range, reset, search, submit, tel, text, time, url, and week
						default :
							// make sure it didn't already have a value
							$new_element = \preg_replace('/value=(\'|")([%&a-zA-Z0-9_;-]+)(\'|")/', '', $matches[0][0]);
							// remember to escape output
							$new_element = \str_replace(" type=", " value=\"".\n\str\encode($value)."\" type=", $new_element);
							$markup = \str_replace($matches[0][0], $new_element, $markup);
							break;
					}
				}
				continue;
			}

			if (\preg_match($regex = "/(<textarea[^>]*((name=\"$key\")|(name='$key'))[^>]*>)(.*?)<\/textarea>/s", $markup, $matches)) {
				// matches[0] is string
				$new_element = $matches[1].\n\str\encode($value).'</textarea>';
				$markup = \str_replace($matches[0], $new_element, $markup);
				continue;
			}

			if (\preg_match($regex = "/<select[^>]*((name=\"$key\")|(name='$key'))[^>]*>(.*?)<\/select>/s", $markup, $matches)) {
				// matches[0] is string
				// make sure it didn't already have a select
				$new_element = \preg_replace('/selected=(\'|")selected(\'|")/', '', $matches[0]);
				$new_element = \str_replace("value=\"$value\"", " selected=\"selected\" value=\"$value\"", $new_element);
				$markup = \str_replace($matches[0], $new_element, $markup);
				continue;
			}
		}

		return $markup;
	}
}
/**
* Security functions, including encryption, decryption.
*/
namespace n\security
{
	/**
	* Encrypts value using Rijndael 256.
	*
	* @param string|int $value
	* @param string|int $key (OPTIONAL) When not set, the encryption_key in nconfig.php is used.
	* @param string|int $salt (OPTIONAL) When not set, the encryption_salt in nconfig.php is used.
	* @return string
	*/
	function encrypt($value, $key = null, $salt = null)
	{
		if ($key === null)
			$key = \n\config\get('encryption_key');

		if ($salt === null)
			$salt = \n\config\get('encryption_salt');

		$join = function($value1,$value2)
		{
			return \base64_encode($value1).'|'.\base64_encode($value2);
		};

		$hash = function($value,$salt) use ($key)
		{
			return \hash_hmac('sha256', $value, $key, true);
		};

		$hashedKey = $hash($key, $salt);
		// time serves as nonce
		$mt = \microtime(true);
		// concat base64'd mt and value
		$dataAndMessage = $join($mt, $value);
		// get mac for data and message
		$mac = $hash($dataAndMessage, $hashedKey);
		$dataAndMessageAndMac = $join($dataAndMessage, $mac);
		$cipherText = \mcrypt_encrypt(\MCRYPT_RIJNDAEL_256, $hashedKey, $dataAndMessageAndMac, 'cbc', $hash($mt, $salt));
		return $join($mt, $cipherText);
	}
	/**
	* Decrypts strings encrypted with the encrypt() function (which uses Rijndael 256.)
	*
	* @param string $value
	* @param string|int $key (OPTIONAL) When not set, the encryption_key in nconfig.php is used.
	* @param string|int $salt (OPTIONAL) When not set, the encryption_salt in nconfig.php is used.
	* @return string|int
	*/
	function decrypt($value, $key = null, $salt = null)
	{
		if ($key === null)
			$key = \n\config\get('encryption_key');

		if ($salt === null)
			$salt = \n\config\get('encryption_salt');

		$break = function($string)
		{
			$elements = \explode('|', $string);
			$elements[0] = \base64_decode($elements[0]);
			$elements[1] = \base64_decode($elements[1]);
			return $elements;
		};

		$hash = function($value,$salt) use ($key)
		{
			return \hash_hmac('sha256', $value, $key, true);
		};
		// sha256_d converts key to 128 bit result in binary
		$hashedKey = $hash($key, $salt);
		list($mt, $cipherText) = $break($value);
		$dataAndMessageAndMac = \mcrypt_decrypt(\MCRYPT_RIJNDAEL_256, $hashedKey, $cipherText, 'cbc', $hash($mt, $salt));
		list($dataAndMessage, $mac) = $break($dataAndMessageAndMac);
		$hmacResult = $hash($dataAndMessage, $hashedKey);

		if($hmacResult != trim($mac)) {
			throw new \Exception('The encrypted value appears to be corrupted.');
		} else {
			list($mt, $message) = $break($dataAndMessage);
			return $message;
		}
	}
}
/**
* SQL select functions.
*/
namespace n\sql\source
{
	/**
	* Creates and returns a connection handle for data sources (i.e., queries that read rows of a table, such as select queries.)
	*
	* If no arguments are supplied, the sql_source values in nconfig.php are used to create the connection and the connection is saved for subsequent sql source queries.
	*
	* @param string $dsn (OPTIONAL)
	* @param string $username (OPTIONAL)
	* @param string $password (OPTIONAL)
	* @return PDO Object handle for the database connection.
	*/
	function connect($dsn = null, $username = null, $password = null)
	{
		static $dbhs = array();

		if (isset($dbhs['default']))
			return $dbhs['default'];
		// if this is a default set entirely in nconfig, save dbh for later use.
		if ($dsn === null) {
			$sql_source = \n\config\get(array('sql_source_dsn','sql_source_username','sql_source_password'));
			$dbh = new \PDO($sql_source['sql_source_dsn'], $sql_source['sql_source_username'], $sql_source['sql_source_password']);
			$dbhs['default'] = $dbh;
		} else {
			if ($username === null)
				$username = \n\config\get('sql_source_username');

			if ($password === null)
				$password = \n\config\get('sql_source_password');

			$dbh = new \PDO($dsn, $username, $password);
		}

		$dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
		return $dbh;
	}

	/**
	* Performs read-only ('SELECT') queries on tables and returns results.
	*
	* @param array $query
	* @param array $inputs (OPTIONAL)
	* @param PDO $dbh (OPTIONAL)
	* @return array
	*/
	function query($query, array $inputs = array(), $dbh = null)
	{
		if ($dbh === null)
			$dbh = connect();

		$sth = $dbh->prepare($query);
		$sth->execute($inputs);
		return $sth->fetchAll(\PDO::FETCH_ASSOC);
	}
}
/**
* SQL insert, updaste, and delete functions.
*/
namespace n\sql\action
{
	/**
	* Creates and returns a connection handle for data actions (i.e., queries that alter rows of the table, such as insert, update, and delete.)
	*
	* If no arguments are supplied, the sql_action values in nconfig.php are used to create the connection and the connection is saved for subsequent sql action queries.
	*
	* @param string $dsn (OPTIONAL)
	* @param string $username (OPTIONAL)
	* @param string $password (OPTIONAL)
	* @return PDO
	*/
	function connect($dsn = null, $username = null, $password = null)
	{
		static $dbhs = array();

		if (isset($dbhs['default']))
			return $dbhs['default'];
		// if this is a default set entirely in nconfig, save dbh for later use.
		if ($dsn === null) {
			$sql_action = \n\config\get(array('sql_action_dsn','sql_action_username','sql_action_password'));
			$dbh = new \PDO($sql_action['sql_action_dsn'], $sql_action['sql_action_username'], $sql_action['sql_action_password']);
			$dbhs['default'] = $dbh;
		} else {
			if ($username === null)
				$username = \n\config\get('sql_action_username');

			if ($password === null)
				$password = \n\config\get('sql_action_password');

			$dbh = new \PDO($dsn, $username, $password);
		}

		$dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
		return $dbh;
	}
	/**
	* Performs queries that alter table rows, such as 'INSERT', 'UPDATE', and 'DELETE' queries.
	*
	* @param array $query
	* @param array $inputs
	* @param PDO $dbh (OPTIONAL)
	* @param string $last_insert_id_name (OPTIONAL) Adds ability to specify name of sequence id, used for PostgreSQL
	* @return mixed Returns the last insert id if an insert operation, true if the query was successful, or throws an exception if the query did not succeed.
	*/
	function query($query, array $inputs, $dbh = null, $last_insert_id_name = null)
	{
		if ($dbh === null)
			$dbh = connect();

		$sth = $dbh->prepare($query);
		$sth->execute($inputs);

		if(strtolower(substr(trim($query), 0, 6)) == 'insert') {
			return $dbh->lastInsertId($last_insert_id_name);
		} else {
			return true;
		}
	}
	/**
	* Convenience function for deleting a row from a table by it's id (must follow the convention that the table primary key is named 'id'.)
	*
	* @param string $table_name
	* @param int $id
	* @param PDO $dbh (OPTIONAL)
	* @return bool Returns true if the query was successful, or throws an exception if the query did not succeed.
	*/
	function delete($table_name, $id, $dbh = null)
	{
		return query("DELETE FROM $table_name WHERE id = ?", array($id), $dbh);
	}
	/**
	* Convenience function for inserting an associative array row into a table (array keys must match the column names of the table.)
	*
	* @param string $table_name
	* @param array $inputs
	* @param PDO $dbh (OPTIONAL)
	* @param string $last_insert_id_name (OPTIONAL) Adds ability to specify name of sequence id, used for PostgreSQL
	* @return int Returns the last insert id or throws an exception if the query did not succeed.
	*/
	function insert($table_name, array $inputs, $dbh = null, $last_insert_id_name = null)
	{
		$keys = array_keys($inputs);
		$fields = implode(',', $keys);
		$place_holders = implode(',', array_map(function($v){return ':'.$v;}, $keys));
		return query("INSERT INTO $table_name ($fields) VALUES ($place_holders)", $inputs, $dbh, $last_insert_id_name);
	}
	/**
	 * Convenience function that automatically determines whether to call insert or update.
	 * @param string $table_name
	 * @param array $inputs
	 * @param PDO $dbh (OPTIONAL)
	 * @param string $last_insert_id_name (OPTIONAL) Adds ability to specify name of sequence id, used for PostgreSQL
	 * @return mixed Returns the last insert id if an insert operation, true if the query was successful, or throws an exception if the query did not succeed.
	 */
	function save($table_name, array $inputs, $dbh = null, $last_insert_id_name = null)
	{
		if (isset($inputs['id'])) {
			return update($table_name, $inputs, $dbh);
		} else {
			return insert($table_name, $inputs, $dbh, $last_insert_id_name);
		}
	}
	/**
	* Convenience function for updating a table row with values from an associative array (array keys must match the column names of the table.)
	*
	* @param string $table_name
	* @param array $inputs
	* @param PDO $dbh (OPTIONAL)
	* @return bool Returns true if the query was successful, or throws an exception if the query did not succeed.
	*/
	function update($table_name, array $inputs, $dbh = null)
	{
		if (!isset($inputs['id']))
			throw new \Exception('The action_update method requires that the table your updating follow the convention that it has a primary key called "id".  No key was found that matched, so try using the action_query() function to perform your update.');

		$keys = array_keys($inputs);
		unset($keys[array_search('id', $keys)]);
		$update_values = implode(',', array_map(function($v){return $v.' = :'.$v;}, $keys));
		return query("UPDATE $table_name SET $update_values WHERE id = :id", $inputs, $dbh);
	}
}
/**
* String manipulation functions.
*/
namespace n\str
{
	/**
	* Wrapper for PHP's str_replace that encodes the replaced values and provides the opportunity to allow string whitelists that shouldn't be encoded.
	*
	* @param string $search
	* @param string $replace
	* @param string $subject
	* @param array $whitelist (OPTIONAL)
	* @param string $charset (OPTIONAL)
	* @return string
	*/
	function replace($search, $replace, $subject, $whitelist = array(), $charset = null)
	{
		return \str_replace($search, encode($replace, $whitelist, $charset), $subject);
	}
	/**
	* HTML-encodes output and allows whitelists of values that should be let through without encoding.
	*
	* @param string $string
	* @param array $allowedHTML (OPTIONAL)
	* @param string $charset (OPTIONAL)
	* @return string
	*/
	function encode($string, $allowed_html = array(), $charset = null)
	{
		static $stored_charset = null;
		static $allowed_html_caches = array();
		// avoids calling n\config\get() every time this function is called
		if (($stored_charset === null) && ($charset === null))
			$stored_charset = \n\config\get($name = 'charset');
		// set charset for future calls
		if ($charset === null)
			$charset = $stored_charset;
		// use htmlspecialchars because it only does the 5 most important chars, and doesn't mess them up
		$encoded_string = \htmlspecialchars($string, \ENT_QUOTES, $charset);

		if ($allowed_html) {
			// cached array variable to be used
			$allowed_html_cache_array = null;
			// find a matching allow_html array in the cache if it exists
			foreach ($allowed_html_caches as $allowed_html_cache) {
				if ($allowed_html_cache['array'] === $allowed_html) {
					$allowed_html_cache_array = $allowed_html_cache;
					break;
				}
			}
			
			if ($allowed_html_cache_array !== null) {
				// found matching allow_html array, so use its members
				foreach ($allowed_html_cache_array['entitied_regexes'] as $entitied_regex) {
					$encoded_string = \preg_replace_callback($entitied_regex, function($matches){return \html_entity_decode($matches[0]);}, $encoded_string);
				}
				
				$encoded_string = \str_replace($allowed_html_cache_array['entitied_tags'], $allowed_html_cache_array['tags'], $encoded_string);
			} else {
				$new_allowed_html_cache = array('entitied_regexes' => array(), 'entitied_tags' => array(), 'tags' => array(), 'array' => $allowed_html);
				
				foreach($allowed_html as $tag) {
					if (($tag[0] == '/') && ($tag[strlen($tag) -1] == '/')) {
						// Save entitied version of regex
						// This should be acceptable because <, >, &, ', ", don't have special meaning in regexes
						$new_allowed_html_cache['entitied_regexes'][] = $entitied_regex = \htmlspecialchars($tag, \ENT_QUOTES, $charset);
						// Tag represents a regular expression of the tag
						$encoded_string = \preg_replace_callback($entitied_regex, function($matches){return \html_entity_decode($matches[0]);}, $encoded_string);
					} else {
						$new_allowed_html_cache['tags'][] = $tag;
						// Save entitied version of tag, then look for occurances of it in string and replace them
						$new_allowed_html_cache['entitied_tags'][] = $entitied_tag = \htmlspecialchars($tag, \ENT_QUOTES, $charset);
						$encoded_string = \str_replace($entitied_tag, $tag, $encoded_string);
					}
				}
				// store cached components
				$allowed_html_caches[] = $new_allowed_html_cache;
			}
		}

		return $encoded_string;
	}
}
/**
* Template capabilities built-in to Nephtali
*/
namespace n\template
{
	/**
	* Register a template for the current page request.
	*
	* Templates are standard (X)HTML files that include regions set aside for replacement.  You can declare multiple templates for a page.
	*
	* @param string $path Include path to the template file.
	* @param array $regions (OPTIONAL) Names of the template regions.
	* @param array $opts (OPTIONAL)
	* @return void
	*/
	function register($path, $regions = array(), $opts = array())
	{
		return \n\z\value($domain = 'template', $command = 'set', $inputs = array('path' => $path, 'regions' => $regions, 'opts' => $opts), $index = 'path');
	}
}
/**
* URL functions, such as redirects, parsing, etc.
*/
namespace n\url
{
	/**
	* Checks if the current url path includes any one of the path arguments.
	*
	* For example, if you wanted to check to see if the page was the admin directory of the site, the $paths argument would be $paths = array('/admin').
	*
	* @param array $paths
	* @return bool
	*/
	function current_path(array $paths)
	{
		foreach ($paths as $path) {
			// Use PHP_SELF so path info could play role, too
			if (\strpos($_SERVER['PHP_SELF'], $path) === 0)
				return true;
		}

		return false;
	}
	/**
	* Immediately redirects from the current page to the URL.
	*
	* @param string $url
	* @return void
	*/
	function redirect($url)
	{
		\ob_clean();
		\header('Location: '.$url);
		exit();
	}
	/**
	* Retrieve the current url.
	*
	* @return string
	*/
	function get_current()
	{
		$url = '';
		$port = '';

		if (isset($_SERVER['HTTP_HOST'])) {
			$colon = \strpos($_SERVER['HTTP_HOST'], ':');

			if ($colon === false) {
				if (isset($_SERVER['SERVER_PORT'])) {
					$port = ':' . $_SERVER['SERVER_PORT'];
				}
				$url = $_SERVER['HTTP_HOST'];
			} else {
				$url = \substr($_SERVER['HTTP_HOST'], 0, $colon);
				$port = \substr($_SERVER['HTTP_HOST'], $colon);
			}
		} elseif(isset($_SERVER['SERVER_NAME'])) {
			$url = $_SERVER['SERVER_NAME'];
			if (isset($_SERVER['SERVER_PORT'])) {
				$port = ':' . $_SERVER['SERVER_PORT'];
			}
		}

		if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
			$url = 'https://' . $url;

			if ($port == ':443')
				$port = '';
		} else {
			$url = 'http://' . $url;

			if ($port == ':80')
				$port = '';
		}

		$url .= $port;

		if (isset($_SERVER['REQUEST_URI'])) {
			$url .= $_SERVER['REQUEST_URI'];
		} else {
			throw new \Exception('We could not retrieve the URL for this page, so you\'ll have to turn off post reloading in nconfig.');
		}

		// nhash used to maintain url #hash values in urls, as this is not available through PHP.
		if (isset($_GET['nhash']))
			$url .= '#'.$_GET['nhash'];

		return $url;
	}
}
/**
 * Function(s) for working with pipe views.
 */
namespace n\view
{
	/**
	* Databinds rows of associative arrays to the markup contained within the <!--data--> region of the pipe markup.
	*
	* @param $markup
	* @param $rows
	* @param array $opts (OPTIONAL)
	* @return string
	*/
	function databind($markup, $rows, array $opts = array())
	{
		if (\count($regions = \n\markup\get_sections($markup, $type = 'data')) == 1) {
			return $markup;
		}

		$data_markup = $regions[1];
		$regions[1] = '';

		if (!isset($opts['databinding']) || $opts['databinding'] == 'placeholder') {
			$is_placeholder = true;
		} elseif ($opts['databinding'] == 'form') {
			$is_placeholder = false;
		} else {
			throw new \Exception('The "databinding" option was not recognized. Please set the value to either "placeholder" (default) or "form".');
		}

		foreach ($rows as $row) {
			$row_markup = $data_markup;

			if (isset($opts['optblocks'])) {
				foreach ($opts['optblocks'] as $block_name => $func) {
					if (\count($block_sections = \n\markup\get_sections($row_markup, $type = 'optblock', $name = $block_name)) != 3) {
						throw new \Exception('The optblock "'.$block_name.'" is missing from the markup.');
					}

					if (!$func($row)) {
						$block_sections[1] = '';
					}

					$row_markup = \implode('', $block_sections);
				}
			}

			if ($is_placeholder) {
				$regions[1] .= \n\row\databind($row_markup, $row, $opts);
			} else {
				$regions[1] .= \n\row\databind_form($row_markup, $row, $opts);
			}
		}

		return \implode('', $regions);
	}
	/**
	* Returns view markup after any required databinding.
	*
	* The $opts argument provides you with several rendering options, all of which are displayed in the example $opts array below:
	* <code>
	* $opts = array(
	*	// allow italics tags to pass through the encoding for the title field
	*	'whitelists' => array('title' => array('<i>','</i>')),
	*
	*	// display rating optblock when rating field is not empty
	*	'optblocks' => array('rating' => function($row){ if (strlen($row['rating'])) return true; return false; }),
	*
	*	// set databinding to "placeholder" (default) or "form", which binds data by matching array keys to form field names
	*	'databinding' => 'form'
	* );
	* </code>
	*
	* @param string $name
	* @param string $markup
	* @param array $rows (OPTIONAL) Accepts an array of associative arrays, or a single associative array in the case of binding one row.
	* @param array $opts (OPTIONAL) Accepts an assocative array containing the rendering options (detailed in the description of this docblock.)
	* @return string
	*/
	function render($name, $markup, $rows = array(), array $opts = array())
	{
		$opts = \array_merge(array('databinding' => 'placeholder'), $opts);

		// if single associative array to bind one row, wrap in container array
		if (\n\arr\get_type($rows) == 'associative' && !(\array_filter($rows, function ($val){return ((array)$val === $val);})))
			$rows = array($rows);

		$markup_sections = \n\markup\get_sections($markup, $type = 'view', $name);

		if (\count($markup_sections) != 3)
			throw new \Exception("We could not find the markup for the view '$name'.  Please make sure you have view comments in place for each of the views your pipes expect.");

		return databind($markup_sections[1], $rows, $opts);
	}
}
/**
* Output functions.
*
* Mechanism in Nephtali for you to output data to regions outside standard pipes.
* Values are immutable and automatically escaped.
*/
namespace n\output
{
	/**
	* Returns the array of immutable Nephtali outputs named in the arugment.
	*
	* @param array $names
	* @return array
	*/
	function get(array $names)
	{
		return \n\z\value($domain = 'output', $command = 'get', $inputs = $names);
	}
	/**
	* Saves an output to be databound to page (values are immutable, so get them right the first time :)
	*
	* @param $name
	* @param $value
	* @return void
	*/
	function register($name, $value)
	{
		\n\z\value($domain = 'output', $command = 'set', $inputs = array('name' => $name, 'value' => $value));
	}
}
/**
* XML functions using SimpleXML to simplify tasks.
*/
namespace n\xml
{
	/**
	* Parse an XML string using functions specific to particular xpath node results.
	*
	* If there is a default namespace (e.g., xmlns="http://website.com"), you don't have to declare a default namespace and use it to reference each element as you normally would.
	*
	* @param string $xml_str
	* @param mixed $result String, array, object returned to the from the function after processing.
	* @param array $xpath_functions Associative array of xpath keys and function values.  Each function must accept a $node argument and a $result argument.
	* @param array $namespaces (OPTIONAL)
	* @return mixed
	*/
	function parse($xml_str, $result, array $xpath_functions, array $namespaces = array())
	{
		// hack to avoid default namespace issues
		$xml = new \SimpleXMLElement(str_replace("xmlns=", "ns=", $xml_str));

		foreach ($namespaces as $key => $value) {
			$xml->registerXPathNamespace($key, $value);
		}

		foreach ($xpath_functions as $xpath => $function) {
			$matches = $xml->xpath($xpath);

			foreach ($matches as $node) {
				$result = $function($node, $result);
			}
		}

		return $result;
	}
}
/**
 * All n\z components are internal.
 *
 * The n\z components are internal and not stable in terms of change to the API.  Use at your own risk!!!
 */
namespace n\z
{
	/**
	* Sets error handling mode for the page.
	*
	* @param int $mode
	*/
	function set_error_handling($mode)
	{
		// E_STRICT will be part of future PHP versions, so might as well start now
		\error_reporting(E_ALL | E_STRICT);
		// Make things exceptions instead of errors (when possible)
		\set_error_handler('n\z\error_handler');

		if ($mode == 'dev' || $mode == 'test') {
			\ini_set('display_errors', '1');
			\set_exception_handler('n\z\dev_handler');
		} else {
			\ini_set('display_errors', '0');
			\set_exception_handler('n\z\prod_handler');
		}
	}
	/**
	* Handles errors by throwing them as exceptions with the underlying info contained in the message.
	*
	* @param int $errno
	* @param string $errstr
	* @param string $errfile
	* @param int $errline
	*/
	function error_handler($errno, $errstr, $errfile, $errline)
	{
		// must take into account error suppressor (@) and not do anything with them (they equal 0)
		// http://framework.zend.com/issues/browse/ZF-3829
		// check against current error_reporting bitmasks
		if (!(\error_reporting() & $errno)) {
			return true;
		} else {
			$error_msg = "<dl><dt>Error Type (see http://www.php.net/manual/en/errorfunc.constants.php):</dt><dd>$errno</dd><dt>Error Message:</dt><dd>$errstr</dd><dt>File:</dt><dd>$errfile</dd><dt>Line:</dt><dd>$errline</dd></dl>";
			throw new \Exception($error_msg);
		}
	}
	/**
	* Handles exceptions not caught by the page when the application is in prod mode.
	* Any output is erased and the user is redirected to the default error page set in nconfig.php.
	* @param void $exception
	*/
	function prod_handler($exception)
	{
		try {
			\n\url\redirect(\n\config\get('error_page'));
		} catch (\Exception $e) {
			exit('Currently, this page cannot be rendered.  Please contact the site administrator.');
		}
	}
	/**
	* Handles exceptions not caught by the page when the application is in dev mode.
	* Details of the exception and a backtrace of the code that led to the exception are output to the screen.
	* @param void $exception
	*/
	function dev_handler($exception)
	{
		try {
			// Get rid of any output that's already buffered
			\ob_end_clean();

?>
<html>
<head>
<title>Debug output for unhandled exception</title>
</head>
<body>
<h1>Debug output for unhandled exception</h1>
<?php echo $exception->getMessage(); ?><br />
<h2>Debug Backtrace</h2>
<pre><?php echo \n\arr\dump(debug_backtrace()); ?></pre>
</body>
</html>
<?php
			die();
		} catch (\Exception $e) {
			echo $exception->getMessage();
		}
	}
	function value($domain, $command, array $inputs, $index = 'name')
	{
		static $values = array();

		if (!isset($values[$domain]))
			$values[$domain] = array();

		if ($command == 'isset') {
			$results = array();

			foreach ($inputs as $key) {
				$results[$key] = isset($values[$domain][$key]);
			}

			return $results;
		}

		if ($command == 'get') {
			if ($inputs) {
				return \array_intersect_key($values[$domain], \array_fill_keys($inputs, 0));
			} else {
				return $values[$domain];
			}
		}

		if ($command == 'set') {
			if (!isset($values[$domain][$inputs[$index]])) {
				$values[$domain][$inputs[$index]] = $inputs;
				return;
			} else {
				throw new \Exception('There is already a '.$values[$domain].' called "'.$inputs[$index].'" that has been registered for this page.');
			}
		}

		throw new \Exception($message = 'The n\z\value() function did not receive one of the valid commands: isset, get, or set.');
	}
}
namespace n\z\bytes
{
	function compute($val)
	{
		$val = \trim($val);

		switch ($last = \strtolower(\substr($val, -1))) {
			// 'G' modifier available since PHP 5.1.0
			case 'g':
				return substr($val, 0, strlen($val) - 1)*1073741824;
			case 'm':
				return substr($val, 0, strlen($val) - 1)*1048576;
			case 'k':
				return substr($val, 0, strlen($val) - 1)*1024;
			default:
				return $val;
		}
	}
	function get_friendly($val)
	{
		if ($val < 1024)
			return $val.' bytes';

		if ($val < 1048576)
			return ($val/1024).'KB';

		if ($val < 1073741824)
			return ($val/1048576).'MB';

		return ($val/1073741824).'GB';
	}
}
namespace n\z\file
{
	/**
	* File exists function that takes into account include path.
	*
	* @param string $file
	* @return bool
	*/
	function exists($file)
	{
		$paths = \explode(PATH_SEPARATOR, get_include_path());

		foreach ($paths as $path) {
			$path = $path.'/'.$file;

			if (\file_exists($path))
				return true;
		}

		return false;
	}
}
namespace n\z\pipe
{
	class Args implements \ArrayAccess, \Iterator, \Countable
	{
		private $_keys = array();
		private $_funcs = array();
		private $_cached_values = array();
		private $_pointer = 0;

		private function _getFuncValue($key)
		{
			if (isset($this->_cached_values[$key])) {
				return $this->_cached_values[$key];
			} else {
				$func = $this->_funcs[$key];
				return $this->_cached_values[$key] = $func();
			}			
		}
		function offsetSet($key, $value)
		{
			$this->_keys[] = $key;
			$this->_funcs[$key] = $value;
		}
		function offsetGet($key)
		{
			return $this->_getFuncValue($key);
		}
		function offsetUnset($key) {
			throw new \Exception("This args array is immutable, so you can not unset() any of the values once set.");
		}
		function offsetExists($offset)
		{
			return isset($this->_funcs[$offset]);
		}
		function current()
		{
			return $this->_getFuncValue($this->_keys[$this->_pointer]);
		}
		function key(){
			return $this->_keys[$this->_pointer];
		}
		function next()
		{
			$this->_pointer++;
		}
		function rewind()
		{
			$this->_pointer = 0;
		}
		function valid()
		{
			return isset($this->_keys[$this->_pointer]);
		}
		function count()
		{
			return count($this->_keys);
		}
	}
	function process_registered($markup)
	{
		// bane of PHP's olden days
		if (get_magic_quotes_gpc()) throw new \Exception ($message = 'Magic quotes is turned on.  Please, oh please, turn it off using php.ini or htaccess.');
		// get configuration set up.
		require('nconfig.php');
		// store mode avoid excessive calls to n\config\get()
		$nephtali_mode = \n\config\get($name = 'mode');
		// configure environment with config settings.
		\n\z\set_error_handling($nephtali_mode);
		// if it's production or development mode, and if nmode isn't set, handle post requests.
		if (($nephtali_mode == 'dev' || $nephtali_mode == 'prod') && !isset($_GET['nmode']))
			\n\z\post\process_request();
		// done with mode so unset to avoid potential issues.
		$nephtali_mode = null;
		// determine processing mode and save any other related info.
		if (isset($_GET['nmode'])) {
			$nephtali_response_mode = $_GET['nmode'];
			
			switch ($nephtali_response_mode) {
				case 'validate':
					if (!isset($_GET['nports']) || !\preg_match('/^([-a-zA-Z0-9_]+(,)?)+$/', $_GET['nports']))
						throw new \Exception('The nports get variable we received was invalid.');

					\n\val($name = 'nephtali_response_mode', $value = 'validate');
					\n\val($name = 'nephtali_validation_ports', $value = \explode(',', $_GET['nports']));
					break;
				case 'debug':
					if (\n\config\get($names = 'mode') == 'dev') {
						\n\val($name = 'nephtali_response_mode', $value = 'debug');
					} else {
						\n\val($name = 'nephtali_response_mode', $value = 'standard');
					}
					break;
				case 'htmlfrag':
					if (!isset($_GET['npipe']) || !\preg_match('/^[-a-zA-Z0-9_]+$/', $_GET['npipe']))
						throw new \Exception('The npipe get variable we received was invalid.');

					\n\val($name = 'nephtali_response_mode', $value = 'htmlfrag');
					\n\val($name = 'nephtali_htmlfrag_pipe', $value = $_GET['npipe']);
					break;
				default :
					throw new \Exception('The nmode GET variable we received was invalid.');
			}
		} else {
			\n\val($name = 'nephtali_response_mode', $value = 'standard');
		}
		// set flag for cache use
		\n\val($name = 'nephtali_is_cacheable', $value = true, $is_mutable = true);
		// register pipes and ports in npipe.php (authentication, etc.)
		require('nregister.php');
		// register code-behind pipes and ports, if present using SCRIPT_NAME to avoid issues when additional path info is provided
		if (\n\z\file\exists('nsite'.$_SERVER['SCRIPT_NAME']))
			require('nsite'.$_SERVER['SCRIPT_NAME']);
		// store nephtali_response_mode to reduce calls to n\val()
		$nephtali_response_mode = \n\val($name = 'nephtali_response_mode');
		// store debug values only if in dev mode and it's not a debug request
		if (\n\config\get($names = 'mode') == 'dev' && $nephtali_response_mode != 'debug')
			store_debug_values();
		// get rid of GET and POST vars so people are required to use ports to handle inputs.
		unset($_GET, $_POST);
		// render according to response type
		if ($nephtali_response_mode == 'validate')
			return render_json_request();

		if ($nephtali_response_mode == 'debug')
			return render_debug_request($markup);

		return render_request($markup, $nephtali_response_mode);
	}
	function render_values($markup)
	{
		$row = \n\z\value($domain = 'output', $command = 'get', $inputs = array());
		$new_row = array();

		foreach ($row as $name => $arr) {
			$new_row[$name] = $arr['value'];
		}

		return \n\row\databind($markup, $new_row, $opts = array());
	}
	function render_json_request()
	{
		if (!$rows = \n\port\validate(\n\val($name = 'nephtali_validation_ports'))) {
			return '[]';
		} else {
			$invalid_values = '[';

			foreach ($rows as $name => $row) {
				$invalid_values .= '["'.$name.'","'.\str_replace('"', '\"', $row['message']).'"]';
			}

			return $invalid_values.']';
		}
	}
	function render_debug_request()
	{
		require('debug.html');
		$markup = \ob_get_clean();
		// keep page from being cached
		\header("Cache-Control: no-cache, must-revalidate");
		\header("Expires: Sat, 1 Jul 2000 06:00:00 GMT");

		if (!isset($_SESSION))
			session_start();

		$sections = \n\markup\get_sections($markup, $type = 'pipe', $name = 'debug');
		$sections[1] = '';

		if (isset($_SESSION['nephtali_debug'])) {
			$requests = \array_reverse($_SESSION['nephtali_debug']);

			foreach($requests as $request) {
				if (!isset($request['page_requested']) || !isset($request['datetime']))
					break;

				$sections[1] .= <<<EOD
<div>
<h2>{$request['page_requested']} <span class="timestamp">{$request['datetime']}</span></h2>

EOD;
				if (isset($request['vars']))
					$sections[1] .= display_watch_info($request['vars']);

				if (isset($request['ports']))
					$sections[1] .= display_port_info($request['ports']);

				if (isset($request['gpcs']))
					$sections[1] .= display_gpcs_info($request['gpcs']);

				$sections[1] .= '</div>';
			}
		}

		return \implode('', $sections);
	}
	function render_request($markup, $current_mode)
	{
		$page_pipes = \n\pipe\get();
		// Only provide cached page for standard requests
		if ($current_mode == 'standard' && current(\n\z\value($domain = 'cache', $command = 'isset', $inputs = array(\n\constant\CACHE_FINAL)))) {
			$cache = current(\n\z\value($domain = 'cache', $command = 'get', $inputs = array(\n\constant\CACHE_FINAL)));
			$get_func = $cache['get_func'];

			if (($cached_page_markup = $get_func($markup)) !== null)
				return $cached_page_markup;
		}
		// Check template cache.
		if ($template_cache_registered = current(\n\z\value($domain = 'cache', $command = 'isset', $inputs = array(\n\constant\CACHE_AFTER_TEMPLATES)))) {
			$cache = \current(\n\z\value($domain = 'cache', $command = 'get', $inputs = array(\n\constant\CACHE_AFTER_TEMPLATES)));
			$get_func = $cache['get_func'];
			$cached_template_markup = $get_func($markup);
		} else {
			$cached_template_markup = null;
		}
		// Apply cache if present, otherwise render normally.
		if ($cached_template_markup !== null) {
			$markup = $cached_template_markup;
		} else {
			$markup = \n\z\template\apply_templates($markup);
		}
		// Save template cache if one has been registered and if the cache isn't still in saved and if the page is cachable.
		if ($template_cache_registered && ($cached_template_markup === null) && \n\val($name = 'nephtali_is_cacheable')) {
			$cache = current(\n\z\value($domain = 'cache', $command = 'get', $inputs = array(\n\constant\CACHE_AFTER_TEMPLATES)));
			$set_func = $cache['set_func'];
			$set_func($markup);
		}

		if ($current_mode == 'standard')
		{
			// on a standard request, preprocess all curl requests in parallel
			$responses = \n\curl\get_contents_parallel(\n\z\value($domain = 'request', $command = 'get', $inputs = array()));

			foreach ($responses as $url => $output) {
				\n\z\value($domain = 'response', $command = 'set', $inputs = array('url' => $url, 'output' => $output), $index = 'url');
			}
		}

		foreach ($page_pipes as $pipe_name => $pipe) {
			if ($current_mode == 'htmlfrag' && \n\val($name = 'nephtali_htmlfrag_pipe') != $pipe_name)
				continue;

			// These pipes don't impact output and don't require any pipe comments in the markup of the page (e.g., authorization, etc.)
			if (!$pipe['opts']['has_output']) {
				if ($current_mode == 'htmlfrag')
					throw new \Exception('The nmode "htmlfrag" cannot be used on a pipe that has "has_output" set to false.');

				render_pipe_function($pipe_name, $pipe);
			} else {
				// only process optional pipe if it's present in markup
				if ($pipe['opts']['is_optional'] && (stripos($haystack = $markup, $needle = '<!--pipe:'.$pipe_name.'-->') === false)) {
					continue;
				}

				$markup_sections = render_pipe_function($pipe_name, $pipe, $markup, $current_mode);

				if ($current_mode == 'standard') {
					$markup = \implode('', $markup_sections);
				} else {
					$markup = $markup_sections[1];
				}
			}
		}

		$markup = render_values($markup);

		if (\n\val($name = 'nephtali_is_cacheable') && ($current_mode == 'standard') && current(\n\z\value($domain = 'cache', $command = 'isset', $inputs = array(\n\constant\CACHE_FINAL)))) {
			$cache = \current(\n\z\value($domain = 'cache', $command = 'get', $inputs = array(\n\constant\CACHE_FINAL)));
			$set_func = $cache['set_func'];
			$set_func($markup);
		}

		return $markup;
	}
	function render_pipe_function($name, $pipe, $markup = null, $current_mode = 'standard')
	{				
		$function = $pipe['function'];
		$names_of_ports_with_errors = $signatures = array();
		$args = new \n\z\pipe\Args();
		
		if ($current_mode == 'htmlfrag' && $pipe['opts']['required_pipes']) {
			$required_pipes = \n\pipe\get($pipe['opts']['required_pipes']);
			
			foreach ($required_pipes as $required_pipe_name => $required_pipe) {
				$markup = implode('', render_pipe_function($required_pipe_name, $required_pipe, $markup, $current_mode));
			}
		}
		
		foreach ($pipe['opts']['signatures'] as $signature_name => $signature_ports) {
			if (!$signature_ports)
				continure;

			$signature_ports_sigs = \n\port\get($names = $signature_ports, $key = 'signature');
			$signatures[$signature_name]['is_valid'] = true;

			foreach ($signature_ports_sigs as $signature_port_name => $signature_port_sig) {
				if (!$signature_port_sig) {
					$signatures[$signature_name]['is_valid'] = false;
					break;
				}
			}

			if ($signatures[$signature_name]['is_valid']) {
				$signatures[$signature_name]['errors'] = \n\port\validate($signature_ports);

				if (!$signatures[$signature_name]['errors']) {
					$signatures[$signature_name]['values'] = \n\port\get($signature_ports);
				} else {
					foreach ($signatures[$signature_name]['errors'] as $error_port_name => $error_port_row) {
						$names_of_ports_with_errors[] = $error_port_name;
					}
					
					$valid_ports = \array_diff($signature_ports, $names_of_ports_with_errors);
					$signatures[$signature_name]['values'] = ($valid_ports) ? \n\port\get($valid_ports) : array();
				}
			}
		}

		foreach ($pipe['args'] as $arg_name => $arg_function) {
			$args[$arg_name] = function ()  use ($arg_function, $signatures) { return $arg_function($signatures); };
		}
		
		if ($markup === null) {
			return $function($args, $signatures);
		} else {
			try{
				$markup_sections = \n\markup\get_sections($markup, $type = 'pipe', $name);
				$markup_sections[1] = $function($markup_sections[1], $args, $signatures);
			} catch (\Exception $e) {
				if (\n\config\get('mode') == 'dev')
					throw $e;

				\n\val($name = 'nephtali_is_cacheable', $value = false);
				$markup_sections[1] = \n\view\render($view = 'error', $markup_sections[1]);
			}
		}
		
		return $markup_sections;
	}
	function store_debug_values()
	{
		if (!isset($_SESSION))
			\session_start();

		if (!isset($_SESSION['nephtali_debug']))
			$_SESSION['nephtali_debug'] = array();

		$values['datetime'] = \date(\DATE_RFC822);
		// use PHP_SELF to display path info, too
		$values['page_requested'] = \htmlspecialchars($_SERVER['PHP_SELF'], \ENT_QUOTES, 'UTF-8');
		// first, store gpcs data
		foreach ($_GET as $key=>$value) {
			$values['gpcs']['get'][$key] = $value;
		}

		foreach ($_POST as $key=>$value) {
			$values['gpcs']['post'][$key] = $value;
		}

		foreach ($_COOKIE as $key=>$value) {
			$values['gpcs']['cookie'][$key] = $value;
		}

		foreach ($_SESSION as $key=>$value) {
			if ($key != 'nephtali_debug')
				$values['gpcs']['session'][$key] = $value;
		}
		// second, store port data
		$ports = \n\z\value($domain = 'port', $command = 'get', $inputs = array());

		foreach ($ports as $name => $value) {
			$values['ports'][$name] = array('name' => $name, 'valid' => $value['valid'], 'value' => $value['value']);
		}
		// add a new values item on the the end of the array
		$_SESSION['nephtali_debug'][] = $values;

		while (\count($_SESSION['nephtali_debug']) > 10) {
			// delete the first item in array
			\array_shift($_SESSION['nephtali_debug']);
		}
	}
	function display_gpcs_info($gpcs)
	{
		$markup = '';

		foreach ($gpcs as $key => $value) {
			$markup .= '<h3>'.$key.'</h3><dl>';

			foreach ($gpcs[$key] as $name => $value) {
				$gpc_value = ((array)$value === $value)? \n\arr\dump($arr = $value) : \n\str\encode($value);
				$markup .= '<dt>'.$name.'</dt>';
				$markup .= '<dd>'.$gpc_value.'</dd>';
			}

			$markup .= '</dl>';
		}

		return $markup;
	}
	function display_port_info($ports)
	{
		$markup = <<<EOD
<h3>Ports</h3>
<table class="ports">
<thead>
<tr>
<th>Port name</th>
<th>Port value</th>
<th>Valid</th>
</tr>
</thead>
<tbody>

EOD;

		foreach ($ports as $values) {
			$charset = \n\config\get($name = 'charset');;
			$value = ((array)$values['value'] === $values['value'])? \n\arr\dump($values['value']) : \n\str\encode($values['value']);
			$markup .= '<tr '.(($values['valid'])?'':'class="invalid"').'><td>'.$values['name'].'</td><td>'.$value.'</td><td>'.(($values['valid'])?'valid':'invalid').'</td></tr>';
		}

		$markup .= '</tbody></table>';
		return $markup;
	}
	function display_watch_info($vars)
	{
		if (!$vars)
			return '';

		$markup = '<h3>Watched Variables</h3><dl>';

		foreach ($vars as $var) {
			$markup .= '<dt>'.$var['name'].'</dt><dd>'.print_r(\n\str\encode($var['value']), true).'</dd>';
		}

		$markup .= '</dl>';
		return $markup;
	}
}
namespace n\z\port
{
	function save($arr)
	{
		if ($arr['type'] == 'char_data') {
			// used for error output
			$formatted_name = \str_replace('_', ' ', \ucfirst($arr['name']));
			$defaults = array(
				'min_length'			=> 1,
				'max_length'			=> 10,
				'min_value'				=> 0,
				'max_value'				=> 9999999999,
				'filter'				=> \n\constant\FILTER_INT,
				'regex'					=> null,
				'encrypted'				=> false,
				'allow_null'			=> false,
				'whitelist'				=> array(),
				'allow_array'			=> false,
				'formatted_name'		=> $formatted_name,
				'error_messages'		=> array(),
				'error_message'			=> null,
				'validators'			=> array(),
				'formatter'				=> null
			);
			$opts = \array_merge($defaults, $arr['opts']);
			$whitelist = "'".$opts['formatted_name']."' only accepts the following values:  ".\implode(',', $opts['whitelist']).".";
			// make sure regex is only set for n\constant\FILTER_CUSTOM
			if (($opts['regex'] !== null) && ($opts['filter'] != \n\constant\FILTER_CUSTOM)) {
				throw new \Exception($message = 'Ports can only set a "regex" option when "filter" is set to n\constant\FILTER_CUSTOM.');
			}

			switch ($opts['filter']) {
				case \n\constant\FILTER_INT:
					$error_message = "'{$opts['formatted_name']}' only accepts numbers.";
					break;
				case \n\constant\FILTER_DOUBLE:
					$error_message = "'{$opts['formatted_name']}' only accepts positive or negative numbers with or without decemals.";
					break;
				case \n\constant\FILTER_ALPHANUM:
					$error_message = "'{$opts['formatted_name']}' only accepts numbers and letters as input.";
					break;
				case \n\constant\FILTER_VAR_NAME:
					$error_message = "'{$opts['formatted_name']}' only accepts numbers, letters, underscores, and dashes as input.";
					break;
				case \n\constant\FILTER_US_PHONE:
					$error_message = "'{$opts['formatted_name']}' only accepts valid US phone numbers as input.";
					break;
				case \n\constant\FILTER_US_ZIP:
					$error_message = "'{$opts['formatted_name']}' only accepts valid US zip codes as input.";
					break;
				case \n\constant\FILTER_URL:
					$error_message = "'{$opts['formatted_name']}' only accepts valid URLs as input.";
					break;
				case \n\constant\FILTER_EMAIL:
					$error_message = "'{$opts['formatted_name']}' only accepts valid email addresses as input.";
					break;
				case \n\constant\FILTER_TEXT:
					$error_message = "'{$opts['formatted_name']}' only accepts a single line of basic text as input (no HTML input allowed.)";
					break;
				case \n\constant\FILTER_TEXT_MULTILINE:
					$error_message = "'{$opts['formatted_name']}' only accepts basic text as input (no HTML input allowed.)";
					break;
				case \n\constant\FILTER_XML:
					$error_message = "'{$opts['formatted_name']}' only accepts text and XML as input.";
					break;
				case \n\constant\FILTER_CUSTOM:
					$error_message = "'{$opts['formatted_name']}' is not valid data.";
					break;
				default:
					$error_message = "The data received was invalid.";
			}

			$default_messages = array(
				'min_length'		=> "'{$opts['formatted_name']}' has a minimum length of  {$opts['min_length']} character(s).",
				'max_length'		=> "'{$opts['formatted_name']}' has a maximum length of  {$opts['max_length']} character(s).",
				'min_value'			=> "'{$opts['formatted_name']}' has a minimum value of  {$opts['min_value']}.",
				'max_value'			=> "'{$opts['formatted_name']}' has a maximum value of  {$opts['max_value']}.",
				'allow_null'		=> "'{$opts['formatted_name']}' is an expected value, and we did not recieve it.",
				'whitelist'			=> $whitelist,
				'allow_array'		=> "'{$opts['formatted_name']}' does not allow arrays of data.",
				'filter'			=> $error_message
			);

			if ($opts['min_length'] == 1)
				$default_messages['min_length'] = "'{$opts['formatted_name']}' is required and cannot be left blank.";

			$opts['error_messages'] = \array_merge($default_messages, $opts['error_messages']);

			if ($opts['encrypted'] && $arr['value'] !== null)
				$arr['value'] = \n\security\decrypt($arr['value']);

			$formatter = $opts['formatter'];
			$arr = validate_chardata(array('name' => $arr['name'], 'formatted_name' => $formatted_name, 'value' => ((($formatter === null) || ($arr['value'] === null))?$arr['value']:$formatter($arr['value'])), 'opts' => $opts));
			\n\z\value($domain = 'port', $command = 'set', $inputs = $arr);
			return;
		} elseif ($arr['type'] == 'file') {
			// used for error output
			$formatted_name = \str_replace('_', ' ', \ucfirst($arr['name']));
			$defaults = array(
				'max_bytes'			=> 1048576,
				'required'			=> true,
				'extensions'		=> array('jpg', 'jpeg', 'gif'),
				'ftp_username'		=> null,
				'ftp_password'		=> null,
				'ftp_domain'		=> null,
				'formatted_name'	=> $formatted_name,
				'error_messages'	=> array()
			);
			$opts = \array_merge($defaults, $arr['opts']);
			$friendly_bytes = \n\z\bytes\get_friendly($opts['max_bytes']);
			$extensions = "'".$opts['formatted_name']."' only accepts files with the following extensions:  ".\implode(',', $opts['extensions']).".";
			$default_messages = array(
				'max_bytes'			=> "'{$opts['formatted_name']}' has a maximum size of $friendly_bytes.",
				'required'			=> "'{$opts['formatted_name']}' is required.",
				'extensions'		=> $extensions,
				'is_upload'			=> "'{$opts['formatted_name']}' does not appear to be an uploaded file."
			);
			$opts['error_messages'] = \array_merge($default_messages, $opts['error_messages']);
			$arr = validate_file(array('name' => $arr['name'], 'formatted_name' => $formatted_name, 'value' => $arr['value'], 'opts' => $opts));
			\n\z\value($domain ='port', $command = 'set', $inputs = $arr);
			return;
		} else {
			throw new \Exception('The type for the port was not recognized.');
		}
	}
	function validate_file(array $arr)
	{
		if (($arr['value'] === null) || ($arr['value']['size'] < 1)) {
			if ($arr['opts']['required'])
				return declare_invalid($arr, $failed_test = 'required');

			return declare_valid($arr);
		}

		if (!\is_uploaded_file($arr['value']['tmp_name']))
			return declare_invalid($arr, $failed_test = 'is_upload');

		if ($arr['value']['size'] > $arr['opts']['max_bytes'])
			return declare_invalid($arr, $failed_test = 'max_bytes');

		$info = \pathinfo($arr['value']['name']);

		if (!in_array($info['extension'], $arr['opts']['extensions']))
			return declare_invalid($arr, $failed_test = 'extensions');

		return declare_valid($arr);
	}
	function validate_chardata(array $arr)
	{
		// check for NULLs
		if ($arr['value'] === null) {
			if ($arr['opts']['allow_null']) {
				return declare_valid($arr);
			} else {
				return declare_invalid($arr, $failed_test = 'allow_null');
			}
		}

		// check for arrays
		if ((array)$arr['value'] === $arr['value']) {
			if (!$arr['opts']['allow_array'])
				return declare_invalid($arr, $failed_test = 'allow_array');

			$values = $arr['value'];

			foreach ($values as $val) {
				// make copy of array
				$temp_arr = $arr;
				// replace value with singular value of this iteration
				$temp_arr['value'] = $val;
				// validate
				$temp_arr = validate_chardata_value($temp_arr);

				if (!$temp_arr['valid']) {
					$temp_arr['values'] = $values;
					return $temp_arr;
				}
			}

			return declare_valid($arr);
		} else {
			return validate_chardata_value($arr);
		}
	}
	function validate_chardata_value($arr)
	{
		// whitelist validation
		if (($arr['opts']['whitelist'])) {
			if (!\in_array($arr['value'], $arr['opts']['whitelist']))
				return declare_invalid($arr, $failed_test = 'whitelist');

			return declare_valid($arr);
		}
		// if value is '' and minLength is 0, don't wast processing
		if ($arr['value'] == '' && $arr['opts']['min_length'] == 0)
			return declare_valid($arr);
		// check max length
		if(\strlen($arr['value']) > $arr['opts']['max_length'])
			return declare_invalid($arr, $failed_test = 'max_length');
		// check min length
		if(\strlen($arr['value']) < $arr['opts']['min_length'])
			return declare_invalid($arr, $failed_test = 'min_length');
		// filter
		switch ($arr['opts']['filter']) {
			case \n\constant\FILTER_INT:
				if (!\preg_match(\n\constant\REGEX_INT, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');
				// also check value if an int
				if (!($arr['value'] >= $arr['opts']['min_value']))
					return declare_invalid($arr, $failed_test = 'min_value');

				if (!($arr['value'] <= $arr['opts']['max_value']))
					return declare_invalid($arr, $failed_test = 'max_value');

				break;
			case \n\constant\FILTER_DOUBLE:
				if (!\preg_match(\n\constant\REGEX_DOUBLE, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');
				// also check value if a double
				if (!($arr['value'] >= $arr['opts']['min_value']))
					return declare_invalid($arr, $failed_test = 'min_value');

				if (!($arr['value'] <= $arr['opts']['max_value']))
					return declare_invalid($arr, $failed_test = 'max_value');

				break;
			case \n\constant\FILTER_ALPHANUM:
				if (!\preg_match(\n\constant\REGEX_ALPHANUM, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_VAR_NAME:
				if (!\preg_match(\n\constant\REGEX_VAR_NAME, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_US_PHONE:
				if (!\preg_match(\n\constant\REGEX_US_PHONE, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_US_ZIP:
				if (!\preg_match(\n\constant\REGEX_US_ZIP, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_URL:
				if (!\preg_match(\n\constant\REGEX_URL, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_EMAIL:
				if (!\preg_match(\n\constant\REGEX_EMAIL, $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_TEXT:
				if (!\preg_match(\n\constant\REGEX_TEXT, $arr['value']) || contains_xss($arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_TEXT_MULTILINE:
				if (!\preg_match(\n\constant\REGEX_TEXT_MULTILINE, $arr['value']) || contains_xss($arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_XML:
				if (!\preg_match(\n\constant\REGEX_XML, $arr['value']) || contains_xss($arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			case \n\constant\FILTER_ALL:
				break;
			case \n\constant\FILTER_CUSTOM:
				if (!preg_match($arr['opts']['regex'], $arr['value']))
					return declare_invalid($arr, $failed_test = 'filter');

				break;
			default:
				throw new \Exception('The filter supplied for the port "'.$arr['name'].'" is not valid.');
		}

		foreach ($arr['opts']['validators'] as $error_message => $func) {
			if (!$func($arr['value']))
				return declare_invalid($arr, $failed_test = 'validator', $error_message);
		}

		return declare_valid($arr);
	}
	function declare_valid(array $arr)
	{
		$arr['valid'] = true;
		$arr['signature'] = true;
		$arr['error_message'] = null;
		return $arr;
	}
	function declare_invalid(array $arr, $failed_test, $error_message = null)
	{
		if ($error_message === null) {
			$arr['error_message'] = $arr['opts']['error_messages'][$failed_test];
		} else {
			$arr['error_message'] = \str_replace($search = '{formatted_name}', $replace = $arr['formatted_name'], $subject = $error_message);
		}

		$arr['valid'] = false;
		($failed_test == 'allow_null')?($arr['signature'] = false):($arr['signature'] = true);
		return $arr;
	}
	function contains_xss($value)
	{
		$xss_hackemes = array(
			'%3c', // <
			'&lt', // <
			'pa==',// <
			'&gt', // >
			'%3E', // >
			'pg==',// >
			'&quo',// "
			'%22', // "
			'ig==',// "
			'#27', // '
			'jw==',// '
			'&#',  // generally not needed, only Davin would complain about this omission
			'\x',  // generally not needed, ditto above
			'\u',  // generally not needed, ditto above
			'javascript',
			'onsubmit',
			'onabort',
			'onblur',
			'onchange',
			'onfocus',
			'onmouse',
			'onload',
			'onreset',
			'onselect',
			'onunload',
			'onerror',
			'onclick',
			'<script'
		);

		$lc_value = \strtolower($value);

		foreach($xss_hackemes as $hackeme) {
			if(\strpos($lc_value, $hackeme)===false) {
				continue;
			} else {
				return true;
			}
		}

		return false;
	}
	function valid_signatures($id, $val = null)
	{
		static $signatures = array();
		
		if ($val === null) {
			return $signatures[$id];
		} else {
			$signatures[$id] = $val;
			return $id;
		}
	}
}
namespace n\z\post
{
	function process_request()
	{
		if (($_POST) && (!$_FILES)) {
			save_vars_to_session();
			// set redirect flag
			\setCookie('nephtali_post_redirect', 1);
			\n\url\redirect($url= \n\url\get_current());
		}

		if(isset($_COOKIE['nephtali_post_redirect']) && $_COOKIE['nephtali_post_redirect'] == 1) {
			create_vars_from_session();
			// unset redirect flag
			\setCookie('nephtali_post_redirect', 0);
		}
	}
	function save_vars_to_session()
	{
		if (!isset($_SESSION))
			\session_start();

		foreach ($_POST as $key=>$value) {
			$_SESSION['nephtali_post_vars'][$key] = $value;
			unset($_POST[$key]);
		}
	}
	function create_vars_from_session()
	{
		if (!isset($_SESSION))
			\session_start();

		if (isset($_SESSION['nephtali_post_vars'])) {
			foreach ($_SESSION['nephtali_post_vars'] as $key=>$value) {
				$_POST[$key] = $value;
				unset($_SESSION['nephtali_post_vars'][$key]);
			}
		}
	}
}
namespace n\z\template
{
	function apply_templates($markup)
	{
		// quickly return if there are no templates
		if (!($templates = \n\z\value($domain ='template', $command = 'get', $inputs = array())))
			return $markup;

		$base_markup = null;
		$base_regions = null;
		// add page $markup to $templates just to save code
		$templates['page_markup'] = array('path' => 'page_markup', 'regions' => array());
		// cycle through templates
		foreach ($templates as $path => $settings) {
			if ($path != 'page_markup') {
				// store current template markup
				require_once($path);
				$current_template_markup = \ob_get_contents();
				\ob_clean();
			} else {
				// use page markup here
				$current_template_markup = $markup;
			}

			if ($base_markup === null) {
				$base_markup = $current_template_markup;
				$base_regions = $settings['regions'];
				continue;
			}

			foreach ($base_regions as $region) {
				// if the current template has the region, it should be override the base template
				if (\strpos($current_template_markup, '<!--region:'.$region.'-->')) {
					// get markup from page
					$current_template_sections = \n\markup\get_sections($current_template_markup, $type = 'region', $region);
					// replace markup in template
					$base_markup_sections = \n\markup\get_sections($base_markup, $type = 'region', $region);
					$base_markup_sections[1] = $current_template_sections[1];
					$base_markup = \implode($base_markup_sections);
					unset($base_regions[$region]);
				}
			}
			// add the regions from the current template to the rendering base
			if ($settings['regions'])
				$base_regions = \array_unique(\array_merge($base_regions, $settings['regions']));
		}

		return $base_markup;
	}
}
namespace
{
	$markup = \ob_get_contents();
	\ob_clean();
	echo n\z\pipe\process_registered($markup);
}

?>